/*
 * Copyright Konstantin Triger <kostat@gmail.com> 
 * 
 * This file is part of Jaque 2 JPA - JAva QUEry 2 JPA library <http://code.google.com/p/jaque/>.
 * 
 * Jaque is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jaque is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package jaque.jpa;

import java.lang.reflect.*;
import java.math.*;
import java.util.*;

import javax.persistence.*;

import jaque.Operation;
import jaque.Queryable;
import jaque.expressions.*;

final class QueryFormatter extends BaseDbExpressionVisitor {
	private final StringBuilder _builder = new StringBuilder();
	// private List<? extends Expression> _currentArgs;
	// private int _aliasCount;
	private List<ConstantExpression> _parameters;
	// private boolean _isJoin;
	private int _depth;
	private boolean _whereStarted;

	private boolean _logicalNot;
	private boolean _methodMadeStatic;

	private final static HashMap<Method, KnownMethod> _knownMethods;
	private final static HashMap<String, Method> _nameMethodMapper;
	private final static Method _objectEquals;

	// static private final HashMap<Class<?>, Class<?>> _primitives;

	private static class KnownMethod {
		public final String Name;
		public final boolean MakeStatic;
		public final boolean MakeMember;

		public KnownMethod(String name) {
			this(name, false, false);
		}

		public KnownMethod(String name, boolean makeStatic, boolean makeMember) {
			Name = name;
			MakeStatic = makeStatic;
			MakeMember = makeMember;
		}
	}

	static {

		// HashMap<Class<?>, Class<?>> primitives = new HashMap<Class<?>,
		// Class<?>>(
		// 12);
		// primitives.put(Boolean.class, Boolean.TYPE);
		// primitives.put(Byte.class, Byte.TYPE);
		// primitives.put(Character.class, Character.TYPE);
		// primitives.put(Double.class, Double.TYPE);
		// primitives.put(Float.class, Float.TYPE);
		// primitives.put(Integer.class, Integer.TYPE);
		// primitives.put(Long.class, Long.TYPE);
		// primitives.put(Short.class, Short.TYPE);
		//
		// primitives.put(BigInteger.class, BigInteger.class);
		// primitives.put(BigDecimal.class, BigDecimal.class);
		//
		// primitives.put(String.class, String.class);
		// primitives.put(Class.class, Class.class);
		//
		// _primitives = primitives;

		HashMap<Method, KnownMethod> knownMethods = new HashMap<Method, KnownMethod>();
		HashMap<String, Method> nameMethodMapper = new HashMap<String, Method>();
		Method objectEquals;

		try {
			Method collectionSize = Collection.class.getMethod("size");
			objectEquals = Object.class.getMethod("equals", Object.class);

			knownMethods.put(Math.class.getMethod("sqrt", Double.TYPE),
					new KnownMethod("SQRT"));
			knownMethods.put(Math.class.getMethod("abs", Double.TYPE),
					new KnownMethod("ABS"));
			knownMethods.put(Math.class.getMethod("abs", Float.TYPE),
					new KnownMethod("ABS"));
			knownMethods.put(Math.class.getMethod("abs", Integer.TYPE),
					new KnownMethod("ABS"));
			knownMethods.put(Math.class.getMethod("abs", Long.TYPE),
					new KnownMethod("ABS"));
			knownMethods.put(String.class.getMethod("length"), new KnownMethod(
					"LENGTH", true, false));
			knownMethods.put(String.class.getMethod("substring", Integer.TYPE,
					Integer.TYPE), new KnownMethod("SUBSTRING", true, false));
			knownMethods.put(collectionSize, new KnownMethod("SIZE", true,
					false));
			nameMethodMapper.put("equals", objectEquals);
			nameMethodMapper.put("size", collectionSize);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}

		_objectEquals = objectEquals;
		_nameMethodMapper = nameMethodMapper;
		_knownMethods = knownMethods;
	}

	// private boolean printKnownMethod(Method m) {
	//
	// String toPrint = _knownMethods.get(m);
	// if (toPrint == null) {
	// if (JPQLFunctionNormalizer.ObjectEquals.equals(m)) {
	// _builder.append(DbExpressionType
	// .toString(_logicalNot ? ExpressionType.NotEqual
	// : ExpressionType.Equal));
	// return true;
	// }
	// return false;
	// }
	//
	// _builder.append(toPrint);
	// return true;
	// }

	private boolean handleKnownMethod(MemberExpression e) {
		if (e.getExpressionType() != ExpressionType.MethodAccess)
			return false;

		Member member = e.getMember();
		KnownMethod known = _knownMethods.get(member);
		if (known == null) {
			Method candidate = _nameMethodMapper.get(member.getName());

			if (candidate == null)
				return false;

			if (!JPQLFunctionNormalizer.equals((Method) member, candidate))
				return false;

			if (candidate == _objectEquals) {
				e.getInstance().apply(this);
				_builder.append(DbExpressionType
						.toString(_logicalNot ? ExpressionType.NotEqual
								: ExpressionType.Equal));
				return true;
			}

			known = _knownMethods.get(candidate);

			assert known != null;
		}

		if (known.MakeStatic) {
			_builder.append(known.Name);
			_builder.append('(');
		}

		Expression instance = e.getInstance();
		if (instance != null)
			instance = instance.apply(this);

		if (known.MakeStatic)
			_methodMadeStatic = true;
		else
			_builder.append(known.Name);

		return true;
	}
	
	@Override
	public Expression visit(AggregateExpression e) {

		_builder.append(e.getAggregateType());
		_builder.append('(');
		
		Expression arg = e.getArgument();
		if (arg == null)
			printAlias(_depth);
		else
			arg.apply(this);
		
		_builder.append(')');
		
		return e;
	}

	@Override
	public Expression visit(SelectExpression e) {
		// boolean isJoin = _isJoin;
		// _isJoin = true;

		boolean root = _depth == 0;

		Expression selector = e.getSelector();

		boolean hasSelector = selector != null;

		if (hasSelector)
			_depth++;

		if (root) {
			_builder.append("SELECT ");
			
			if (selector == null)
				printAlias(_depth);
			else
				selector.apply(this);
		}

		e.getFrom().apply(this);

		if (!root && hasSelector) {
			_builder.append("JOIN");
			selector.apply(this);
			printAlias(_depth - 1);
		}

		// ConstantExpression from = getFrom(e);

		// String alias = "t" + _depth;

		// Class<?> entityType = ((JaqueEntityManager.JpaQueryable<?>) from
		// .getValue()).getEntityType();

		// Class<?> entityType1 = selector.getResultType();
		// Class<?> entityType =
		// selector.getParameters().get(0).getResultType();
		//		
		// NamedValueExpression nve = new NamedValueExpression(alias, Expression
		// .constant(null, entityType));
		// if (selector != null) {
		// Expression reduced = Expression.invoke(selector, nve).apply(
		// new ExpressionInliner()); //LambdaExpressionReducer.Instance
		// reduced.apply(this);
		// result.apply(this);
		// }

		// _builder.append(" FROM ");
		// _builder.append(entityType.getName());
		// _builder.append(" AS ");
		// _builder.append(' ');
		// _builder.append(alias);
		// _builder.append(' ');

		Expression where = e.getWhere();
		if (where != null) {
			// Expression result = Expression
			// .invoke(where, nve).apply(new ExpressionInliner());
			// //LambdaExpressionReducer.Instance
			if (_whereStarted)
				_builder.append(" AND ");
			else {
				_builder.append(" WHERE ");
				_whereStarted = true;
			}
			// _builder.append('(');
			// VisitPredicate(result);
			VisitPredicate(where);
			// _builder.append(')');
		}

		if (hasSelector)
			_depth--;

		return e;
	}

	// @Override
	// public Expression visit(InvocationExpression e) {
	// List<? extends Expression> previosArgs = _currentArgs;
	// _currentArgs = e.getArguments();
	// try {
	// // super.visit(e);
	// e.getMethod().apply(this);
	// return e;
	// } finally {
	// _currentArgs = previosArgs;
	// }
	// }

	@Override
	protected List<Expression> visitExpressionList(List<Expression> original) {

		boolean firstParam = !_methodMadeStatic;
		_methodMadeStatic = false;

		boolean p = original.size() > 0;
		if (p && firstParam)
			_builder.append('(');

		for (int i = 0, n = original.size(); i < n; i++) {
			if (firstParam)
				firstParam = false;
			else
				_builder.append(',');

			original.get(i).apply(this);
		}

		if (p || !firstParam)
			_builder.append(')');

		return original;
	}

	@Override
	public Expression visit(MemberExpression e) {

		if (handleKnownMethod(e))
			return e;

		Expression instance = e.getInstance();
		if (instance != null)
			instance = instance.apply(this);

		Member member = e.getMember();
		String name = member.getName();
		if (member instanceof Field) {
			_builder.append('.');
			_builder.append(name);
		} else if (member instanceof Method) {
			// if (!printKnownMethod((Method) member)) {
			if (e.getParameters().size() == 0) {
				// throw new UnsupportedOperationException(name);

				_builder.append('.');

				if (name.startsWith("get")) { // property
					_builder.append(Character.toLowerCase(name.charAt(3)));
					_builder.append(name, 4, name.length());
				} else
					_builder.append(name);
			}
			// }
		} else if (member instanceof Constructor) {
			_builder.append("NEW ");
			_builder.append(name);
		} else
			throw new UnsupportedOperationException(name);

		// visitExpressionList(e.getParameters());

		return e;
	}

	@Override
	public Expression visit(NamedValueExpression e) {
		_builder.append(e.getName());
		_builder.append(' ');
		return e;
	}

	// @Override
	// public Expression visit(ParameterExpression e) {
	// _currentArgs.get(e.getIndex()).apply(this);
	// return e;
	// }

	@Override
	public Expression visit(ParameterExpression e) {
		// _builder.append(" FROM ");
		// _builder.append(e.getResultType().getName());
		printAlias(_depth);
		return e;
		// TODO Auto-generated method stub
		// return super.visit(e);
	}

	@Override
	public Expression visit(ConstantExpression e) {

		if (e.getValue() instanceof Queryable<?>) {
			Queryable<?> q = (Queryable<?>) e.getValue();
			_builder.append(" FROM ");
			_builder.append(q.getElementType().getName());
			printAlias(_depth);
			return e;
		}
		// Class<?> resultType = e.getResultType();
		// if (!resultType.isPrimitive() && _primitives.get(resultType) == null)
		// return e; // not parameter

		int index;
		if (_parameters == null) {
			_parameters = new ArrayList<ConstantExpression>();
			_parameters.add(e);
			index = 1;
		} else {
			index = _parameters.indexOf(e);
			if (index < 0) {
				_parameters.add(e);
				index = _parameters.size();
			} else
				index++; // should be 1 based
		}

		_builder.append('?');
		_builder.append(index);
		_builder.append(' ');

		return e;
	}

	@Override
	public Expression visit(BinaryExpression e) {

		_builder.append('(');
		switch (e.getExpressionType()) {
		case ExpressionType.Modulo:
			_builder.append("MOD(");
			e.getFirst().apply(this);
			_builder.append(',');
			e.getSecond().apply(this);
			_builder.append(')');
			break;
		default:
			e.getFirst().apply(this);
			_builder.append(' ');
			_builder.append(DbExpressionType.toString(e.getExpressionType()));
			_builder.append(' ');
			e.getSecond().apply(this);
			break;
		}
		_builder.append(')');

		return e;
	}

	@Override
	public Expression visit(UnaryExpression e) {
		switch (e.getExpressionType()) {
		case ExpressionType.IsNull:
			_builder.append('(');
			e.getFirst().apply(this);
			_builder.append(' ');
			_builder.append("IS ");
			if (_logicalNot)
				_builder.append("NOT ");
			_builder.append("NULL");
			// _builder.append(DbExpressionType.toString(e.getExpressionType()));
			_builder.append(')');
			break;
		case ExpressionType.LogicalNot:
			if (IsBoolean(e.getResultType())) {
				_logicalNot = true;
				e.getFirst().apply(this);
				_logicalNot = false;
				// should be always IsNull
				// _builder.append('(');
				// ((UnaryExpression) e.getFirst()).getFirst().apply(this);
				// _builder.append(' ');
				// _builder.append("IS NOT NULL");
				// _builder.append(')');
				break;
			}
		case ExpressionType.Convert:
		case ExpressionType.Negate:
		default:
			_builder.append(DbExpressionType.toString(e.getExpressionType()));
			e.getFirst().apply(this);
			break;
		}
		return e;
	}

	@Override
	public String toString() {
		return _builder.toString();
	}

	public Query createQuery(EntityManager em) {
		Query query = em.createQuery(toString());
		if (_parameters != null)
			for (int i = 0; i < _parameters.size(); i++)
				query = query
						.setParameter(i + 1, _parameters.get(i).getValue());

		return query;
	}

	// private static ConstantExpression getFrom(SelectExpression se) {
	// Expression e = se.getFrom();
	// if (e.getExpressionType() == ExpressionType.Constant)
	// return (ConstantExpression) e;
	//
	// return getFrom((SelectExpression) e);
	// }

	void VisitPredicate(Expression expr) {
		expr.apply(this);
		if (!IsPredicate(expr))
			_builder.append(" = TRUE");
	}

	private void printAlias(int index) {
		_builder.append(' ');
		_builder.append('t');
		_builder.append(index);
		_builder.append(' ');
	}

	private static boolean IsBoolean(Class<?> type) {
		return type == Boolean.class || type == Boolean.TYPE;
	}

	private static boolean IsPredicate(Expression e) {
		switch (e.getExpressionType()) {
		// case ExpressionType.And:
		case ExpressionType.LogicalAnd:
			// case ExpressionType.Or:
		case ExpressionType.LogicalOr:
			// return IsBoolean(((BinaryExpression)expr).Type);
		case ExpressionType.LogicalNot:
			// return IsBoolean(((UnaryExpression)expr).Type);
		case ExpressionType.Equal:
		case ExpressionType.IsNull:
		case ExpressionType.NotEqual:
		case ExpressionType.LessThan:
		case ExpressionType.LessThanOrEqual:
		case ExpressionType.GreaterThan:
		case ExpressionType.GreaterThanOrEqual:

		case DbExpressionType.Between:
		case DbExpressionType.Exists:
		case DbExpressionType.In:
			return true;
		case ExpressionType.MethodAccess:
		case ExpressionType.FieldAccess:
			return IsBoolean(e.getResultType());
		case ExpressionType.Invoke:
			return IsPredicate(((InvocationExpression) e).getMethod());
		case ExpressionType.Lambda:
			return IsPredicate(((LambdaExpression<?>) e).getBody());
		default:
			return false;
		}
	}

	// private String getNextAlias() {
	// return "t" + _aliasCount++;
	// }
}
